// Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
// This source file is part of the Cangjie project, licensed under Apache-2.0
// with Runtime Library Exception.
//
// See https://cangjie-lang.cn/pages/LICENSE for license information.


#include <getopt.h>
#include <iostream>

#include "Path.h"
#include "Option.h"

using namespace CangjieMigrator;

const std::string RED_ERROR_MARK = "\x1b[31merror\x1b[0m: ";

template <typename Arg>
static void Println(Arg &&arg)
{
    std::cout << std::forward<Arg>(arg) << std::endl;
}

template <typename... Args>
static void Error(Args &&...args)
{
    std::cerr << RED_ERROR_MARK;
    ((std::cerr << args), ...);
}

void Option::PrintUsage()
{
    Println("Usage: ");
    Println("     cjm <input-file> [-s 0.53.18] [-t latest] [-o <output-file>]");
    Println("     cjm <input-project-dir> [-s 0.53.18] [-t latest] [-o <output-project-dir>]");
    Println("Options:");
    Println("   -h            Show usage");
    Println("   -s <version>  Source cangjie version, "
            "which is upgraded from the current version to the version specified by -t.");
    Println("                 default: earliest");
    Println("   -t <version>  Target cangjie version, "
            "which is upgraded from the version specified by -s to the target version.");
    Println("                 default: latest");
    Println("   -o <target>   Output. If a single file is upgraded, "
            "'-o' is followed by the file or directory name. Relative and absolute paths are supported.");
    Println("                 default: input-self");
    Println("");
    Println("                 eg: cjm a.cj -s 0.53.18 -t latest -o ./a_output.cj");
    Println("                 eg: cjm ~/test_project -s 0.53.18 -t latest -o ./test_project_output");
    Println("                 eg: cjm ~/test_project -s 0.53.18 -t latest");
}

bool Option::IsFileMode() const
{
    return mode == Mode::FILE;
}

bool Option::IsProjectMode() const
{
    return mode == Mode::PROJECT;
}

bool Option::IsDevecoMode() const
{
    return mode == Mode::DEVECO;
}

UpdaterMode Option::GetUpdaterMode() const
{
    switch (mode) {
        case Mode::FILE:
            return UPDATER_MODE_FILE;
        case Mode::PROJECT:
            return UPDATER_MODE_PROJECT;
        case Mode::DEVECO:
            return UPDATER_MODE_DEVECO;
        default:
            throw std::invalid_argument("Invalid mode");
    }
}

bool Option::IsHelp() const
{
    return isHelp;
}

std::string Option::ToString() const
{
    std::string result;
    result += "InputPath: " + inputPath.string() + '\n';
    result += "OutputPath: " + outputPath.string() + '\n';
    result += "SourceVersion: " + sourceVersion + '\n';
    result += "TargetVersion: " + targetVersion + '\n';
    result += "Mode: " + [](const Mode mode) -> std::string {
        switch (mode) {
            case Mode::FILE:
                return "File";
            case Mode::PROJECT:
                return "Project";
            case Mode::DEVECO:
                return "Deveco";
            default:
                return "Unknown";
        }
    }(mode);

    return result;
}

static bool ValidateVersion(Option &option)
{
    if (option.sourceVersion == option.targetVersion) {
        std::cerr << "Please given different source & target version." << std::endl;
        return false;
    }
    if (option.sourceVersion != "earliest" && option.sourceVersion != "0.53.18") {
        std::cerr << "SourceVersion " << option.sourceVersion << " not support." << std::endl;
        return false;
    }
    if (option.targetVersion != "latest") {
        std::cerr << "TargetVersion " << option.targetVersion << " not support." << std::endl;
        return false;
    }
    return true;
}

static bool ValidateInputPath(Option &option)
{
    if (option.inputPath.string().empty()) {
        std::cerr << "Please input a cangjie source file, "
                     "a cangjie project or a deveco project directory." << std::endl;
        return false;
    }
    if (!exists(option.inputPath)) {
        std::cerr << "Input path " << option.inputPath << " is not exist" << std::endl;
        return false;
    }
    if (!is_directory(option.inputPath)) {
        option.mode = Option::Mode::FILE;
        return true;
    }

    if (exists(option.inputPath / "cjpm.toml")) {
        option.mode = Option::Mode::PROJECT;
        return true;
    }

    if (exists(option.inputPath / "hvigorfile.ts")) {
        option.mode = Option::Mode::DEVECO;
        return true;
    }

    std::cerr << "Please input a cangjie source file, "
                 "a cangjie project or a deveco project directory." << std::endl;
    return false;
}

static bool ValidateOuputPath(Option &option)
{
    if (option.outputPath.string().empty()) {
        option.outputPath = option.inputPath;
        return true;
    }
    if (!exists(option.outputPath)) {
        return true;
    }

    if (option.mode == Option::Mode::DEVECO && option.outputPath == option.inputPath) {
        std::cerr << "When upgrading the Deveco project, "
                     "you cannot specify a path that differs from the input path." << std::endl;
        return false;
    }

    if (is_directory(option.outputPath)) {
        if (option.mode == Option::Mode::FILE) {
            std::cerr << "There already exist a directory which has same name with output file " << option.outputPath
                      << std::endl;
            return false;
        }
    } else {
        if (option.mode == Option::Mode::PROJECT) {
            std::cerr << "There already exist a file which has same name with output directory " << option.outputPath
                      << std::endl;
            return false;
        }
    }

    return true;
}

static bool ValidateOption(Option &option)
{
    using ValidateFunc = bool (*)(Option &);
    constexpr ValidateFunc funcs[] = {
        ValidateVersion,
        ValidateInputPath,
        ValidateOuputPath,
    };

    for (const auto func : funcs) {
        if (!func(option)) {
            return false;
        }
    }
    return true;
}

bool Option::ParseArgs(int argc, ArgvType argv)
{
    int ch;
    constexpr char optString[] = "-hs:t:o:";
    while ((ch = getopt(argc, argv, optString)) != -1) {
        switch (ch) {
            case 'h':
                PrintUsage();
                isHelp = true;
                return true;
            case 's':
                sourceVersion = optarg;
                break;
            case 't':
                targetVersion = optarg;
                break;
            case 'o':
                outputPath = optarg;
                break;
            default:
                if (optarg && optarg[0] != '-' && inputPath.empty()) {
                    inputPath = optarg;
                    break;
                }
                if (optopt) {
                    Error("Invalid option or option requires an argument: -", (char)optopt, "\n",
                          "Try: 'cjm -h' for more information.\n");
                } else {
                    Error("Invalid option or option requires an argument\n",
                          "Try: 'cjm -h' for more information.\n");
                }
                return false;
        }
    }

    if (optind < argc) {
        inputPath = argv[optind];
    }

    return ValidateOption(*this);
}
